package org.unidata.mdm.meta.service.impl.data.instance;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntitiesGroupElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.LookupElement;
import org.unidata.mdm.core.type.model.LookupLinkElement;
import org.unidata.mdm.core.type.model.NestedElement;
import org.unidata.mdm.core.type.model.RegisterElement;
import org.unidata.mdm.core.type.model.RelationElement;
import org.unidata.mdm.core.type.model.instance.AbstractModelInstanceImpl;
import org.unidata.mdm.meta.configuration.TypeIds;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.instance.SourceSystemsInstance;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.meta.type.model.entities.EntitiesGroup;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.meta.util.ModelUtils;

/**
 * @author Mikhail Mikhailov on Oct 16, 2020
 */
public class DataModelInstanceImpl extends AbstractModelInstanceImpl<DataModel> implements DataModelInstance {
    /**
     * Registers.
     */
    private final Map<String, RegisterElement> registers;
    /**
     * Lookups.
     */
    private final Map<String, LookupElement> lookups;
    /**
     * Relations.
     */
    private final Map<String, RelationElement> relations;
    /**
     * Nested entities.
     */
    private final Map<String, NestedElement> nested;
    /**
     * Entity groups.
     */
    private final Map<String, EntitiesGroupElement> groups = new HashMap<>();
    /**
     * The root group.
     */
    private final EntitiesGroupElement root;
    /**
     * Constructor.
     */
    public DataModelInstanceImpl(DataModel model, SourceSystemsInstance ssi) {
        super(model);

        // Create siblings map for references resolution
        Map<String, LookupEntity> siblings = model.getLookupEntities().stream()
                .collect(Collectors.toMap(LookupEntity::getName, Function.identity()));

        // Init in the order:
        // - Lookups
        this.lookups = model.getLookupEntities().stream()
                .map(l -> new LookupImpl(l, ssi.getDescendingMap(), siblings))
                .collect(Collectors.toMap(EntityElement::getName, Function.identity()));

        // Post-process lookups due to circular dependencies and no order guarantees.
        wireLookups();

        // - Nested
        this.nested = model.getNestedEntities().stream()
                .map(e -> new NestedImpl(e, model.getNestedEntities(), this))
                .collect(Collectors.toMap(EntityElement::getName, Function.identity()));

        // - Post-process nesteds due to circular dependencies and no order guarantees
        wireNested();

        // - Entities
        this.registers = model.getEntities().stream()
                .map(e -> new RegisterImpl(e, model.getNestedEntities(), ssi.getDescendingMap(), this))
                .collect(Collectors.toMap(EntityElement::getName, Function.identity()));

        // - Relations
        this.relations = model.getRelations().stream()
                .map(r -> new RelationImpl(r, this))
                .collect(Collectors.toMap(EntityElement::getName, Function.identity()));

        // - Entity groups
        EntitiesGroup group = model.getEntitiesGroup();
        if (group == null) {
            group = ModelUtils.DEFAULT_ROOT_GROUP;
        }

        this.root = new EntitiesGroupImpl(StringUtils.EMPTY, group, this);
        putGroups(this.root);

        model.getEntities().stream()
                .filter(entity -> entity.getGroupName() != null)
                .forEach(entity -> {
            EntitiesGroupImpl wrapper = (EntitiesGroupImpl) groups.get(entity.getGroupName());
            if (Objects.nonNull(wrapper)) {
                wrapper.addRegister(registers.get(entity.getName()));
            }
        });

        model.getLookupEntities().stream()
                .filter(entity -> entity.getGroupName() != null)
                .forEach(entity -> {
            EntitiesGroupImpl wrapper = (EntitiesGroupImpl) groups.get(entity.getGroupName());
            if (Objects.nonNull(wrapper)) {
                wrapper.addLookup(lookups.get(entity.getName()));
            }
        });
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isEmpty() {
        return MapUtils.isEmpty(registers)
            && MapUtils.isEmpty(lookups)
            && MapUtils.isEmpty(relations)
            && MapUtils.isEmpty(nested);
    }

    private void wireLookups() {

        for (LookupElement eme : this.lookups.values()) {

            LookupImpl fromLookup = (LookupImpl) eme;

            // process relations lookup -> lookup
            for (Entry<String, AttributeElement> e : fromLookup.getAttributes().entrySet()) {

                if (e.getValue().isLookupLink()) {

                    LookupLinkElement flle = e.getValue().getLookupLink();
                    LookupImpl toLookup = (LookupImpl) this.lookups.get(flle.getLookupLinkName());

                    Objects.requireNonNull(toLookup, "Lookup link attribute cannot reference a null object.");

                    toLookup.addReferencingLookup(fromLookup, e.getValue());
                    fromLookup.addReferencedLookup(toLookup, e.getValue());
                }
            }
        }
    }

    private void wireNested() {

        for (NestedElement nel : this.nested.values()) {

            NestedImpl reference = (NestedImpl) nel;

            // process nested -> nested
            for (AttributeElement ae : reference.getAttributes().values()) {

                if (ae.isComplex()) {

                    NestedImpl target = (NestedImpl) this.nested.get(ae.getComplex().getNestedEntityName());
                    Objects.requireNonNull(target, "Complex attribute cannot reference a null object.");

                    reference.addReferencedNested(target);
                    target.addReferencingNested(reference);
                }
            }
        }
    }

    private void putGroups(EntitiesGroupElement root) {
        groups.put(root.getPath(), root);
        for (EntitiesGroupElement el : root.getChildren()) {
            putGroups(el);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public DataModel toSource() {
        return new DataModel()
                .withCreateDate(getCreateDate())
                .withCreatedBy(getCreatedBy())
                .withVersion(getVersion())
                .withEntities(registers.values().stream()
                        .map(RegisterImpl.class::cast)
                        .map(RegisterImpl::getSource)
                        .collect(Collectors.toList()))
                .withLookupEntities(lookups.values().stream()
                        .map(LookupImpl.class::cast)
                        .map(LookupImpl::getSource)
                        .collect(Collectors.toList()))
                .withNestedEntities(nested.values().stream()
                        .map(NestedImpl.class::cast)
                        .map(NestedImpl::getSource)
                        .collect(Collectors.toList()))
                .withRelations(relations.values().stream()
                        .map(RelationImpl.class::cast)
                        .map(RelationImpl::getSource)
                        .collect(Collectors.toList()))
                .withEntitiesGroup((((EntitiesGroupImpl) root).getSource()));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getInstanceId() {
        return ModelUtils.DEFAULT_MODEL_INSTANCE_ID;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getTypeId() {
        return TypeIds.DATA_MODEL;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<RegisterElement> getRegisters() {
        return registers.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<LookupElement> getLookups() {
        return lookups.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<RelationElement> getRelations() {
        return relations.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<NestedElement> getNested() {
        return nested.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<EntitiesGroupElement> getGroups() {
        return groups.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isElement(String id) {
        return Objects.nonNull(getElement(id));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isRegister(String id) {
        return registers.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isLookup(String id) {
        return lookups.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isRelation(String id) {
        return relations.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isNested(String id) {
        return nested.containsKey(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public EntityElement getElement(String id) {

        EntityElement result = getLookup(id);
        if (Objects.isNull(result)) {

            result = getRegister(id);
            if (Objects.isNull(result)) {

                result = getRelation(id);
                if (Objects.isNull(result)) {

                    result = getNested(id);
                }
            }
        }

        return result;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public RegisterElement getRegister(String id) {
        return registers.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public LookupElement getLookup(String id) {
        return lookups.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public RelationElement getRelation(String id) {
        return relations.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public NestedElement getNested(String id) {
        return nested.get(id);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public EntitiesGroupElement getGroup(String path) {
        return groups.get(path);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public EntitiesGroupElement getRootGroup() {
        return root;
    }
}
